// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <stdint.h>

#include "base/files/file_util.h"
#include "base/memory/ptr_util.h"
#include "base/synchronization/waitable_event.h"
#include "base/sys_byteorder.h"
#include "content/browser/renderer_host/media/audio_debug_file_writer.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/test/test_browser_thread_bundle.h"
#include "media/base/audio_bus.h"
#include "media/base/test_helpers.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace content {

namespace {

    static const uint16_t kBytesPerSample = sizeof(uint16_t);
    static const uint16_t kPcmEncoding = 1;
    static const size_t kWavHeaderSize = 44;

    uint16_t ReadLE2(const char* buf)
    {
        return static_cast<uint8_t>(buf[0]) | (static_cast<uint8_t>(buf[1]) << 8);
    }

    uint32_t ReadLE4(const char* buf)
    {
        return static_cast<uint8_t>(buf[0]) | (static_cast<uint8_t>(buf[1]) << 8) | (static_cast<uint8_t>(buf[2]) << 16) | (static_cast<uint8_t>(buf[3]) << 24);
    }

} // namespace

// <channel layout, sample rate, frames per buffer, number of buffer writes
typedef std::tr1::tuple<media::ChannelLayout, int, int, int>
    AudioDebugFileWriterTestData;

class AudioDebugFileWriterTest
    : public testing::TestWithParam<AudioDebugFileWriterTestData> {
public:
    AudioDebugFileWriterTest()
        : thread_bundle_(content::TestBrowserThreadBundle::REAL_FILE_THREAD)
        , params_(media::AudioParameters::Format::AUDIO_PCM_LINEAR,
              std::tr1::get<0>(GetParam()),
              std::tr1::get<1>(GetParam()),
              kBytesPerSample * 8,
              std::tr1::get<2>(GetParam()))
        , writes_(std::tr1::get<3>(GetParam()))
        , source_samples_(params_.frames_per_buffer() * params_.channels() * writes_)
        , source_interleaved_(source_samples_ ? new int16_t[source_samples_]
                                              : nullptr)
    {
        InitSourceInterleaved(source_interleaved_.get(), source_samples_);
    }

protected:
    virtual ~AudioDebugFileWriterTest() { }

    static void InitSourceInterleaved(int16_t* source_interleaved,
        int source_samples)
    {
        if (source_samples) {
            // equal steps to cover int16_t range of values
            int16_t step = 0xffff / source_samples;
            int16_t val = std::numeric_limits<int16_t>::min();
            for (int i = 0; i < source_samples; ++i, val += step)
                source_interleaved[i] = val;
        }
    }

    static void VerifyHeader(const char (&wav_header)[kWavHeaderSize],
        const media::AudioParameters& params,
        int writes,
        int64_t file_length)
    {
        uint32_t block_align = params.channels() * kBytesPerSample;
        uint32_t data_size = static_cast<uint32_t>(params.frames_per_buffer() * params.channels() * writes * kBytesPerSample);
        // Offset Length  Content
        //  0      4     "RIFF"
        EXPECT_EQ(0, strncmp(wav_header, "RIFF", 4));
        //  4      4     <file length - 8>
        ASSERT_GT(file_length, 8);
        EXPECT_EQ(static_cast<uint64_t>(file_length - 8), ReadLE4(wav_header + 4));
        EXPECT_EQ(static_cast<uint32_t>(data_size + kWavHeaderSize - 8),
            ReadLE4(wav_header + 4));
        //  8      4     "WAVE"
        // 12      4     "fmt "
        EXPECT_EQ(0, strncmp(wav_header + 8, "WAVEfmt ", 8));
        // 16      4     <length of the fmt data> (=16)
        EXPECT_EQ(16U, ReadLE4(wav_header + 16));
        // 20      2     <WAVE file encoding tag>
        EXPECT_EQ(kPcmEncoding, ReadLE2(wav_header + 20));
        // 22      2     <channels>
        EXPECT_EQ(params.channels(), ReadLE2(wav_header + 22));
        // 24      4     <sample rate>
        EXPECT_EQ(static_cast<uint32_t>(params.sample_rate()),
            ReadLE4(wav_header + 24));
        // 28      4     <bytes per second> (sample rate * block align)
        EXPECT_EQ(static_cast<uint32_t>(params.sample_rate()) * block_align,
            ReadLE4(wav_header + 28));
        // 32      2     <block align>  (channels * bits per sample / 8)
        EXPECT_EQ(block_align, ReadLE2(wav_header + 32));
        // 34      2     <bits per sample>
        EXPECT_EQ(kBytesPerSample * 8, ReadLE2(wav_header + 34));
        // 36      4     "data"
        EXPECT_EQ(0, strncmp(wav_header + 36, "data", 4));
        // 40      4     <sample data size(n)>
        EXPECT_EQ(data_size, ReadLE4(wav_header + 40));
    }

    // |result_interleaved| is expected to be little-endian.
    static void VerifyDataRecording(const int16_t* source_interleaved,
        const int16_t* result_interleaved,
        int16_t source_samples)
    {
        // Allow mismatch by 1 due to rounding error in int->float->int
        // calculations.
        for (int i = 0; i < source_samples; ++i)
            EXPECT_LE(std::abs(static_cast<int16_t>(
                                   base::ByteSwapToLE16(source_interleaved[i]))
                          - result_interleaved[i]),
                1)
                << "i = " << i << " source " << source_interleaved[i] << " result "
                << result_interleaved[i];
    }

    void VerifyRecording(const base::FilePath& file_path)
    {
        base::File file(file_path, base::File::FLAG_OPEN | base::File::FLAG_READ);
        EXPECT_TRUE(file.IsValid());

        char wav_header[kWavHeaderSize];
        EXPECT_EQ(file.Read(0, wav_header, kWavHeaderSize),
            static_cast<int>(kWavHeaderSize));
        VerifyHeader(wav_header, params_, writes_, file.GetLength());

        if (source_samples_ > 0) {
            std::unique_ptr<int16_t[]> result_interleaved(
                new int16_t[source_samples_]);
            memset(result_interleaved.get(), 0, source_samples_ * kBytesPerSample);

            // Recording is read from file as a byte sequence, so it stored as
            // little-endian.
            int read = file.Read(kWavHeaderSize,
                reinterpret_cast<char*>(result_interleaved.get()),
                source_samples_ * kBytesPerSample);
            EXPECT_EQ(static_cast<int>(file.GetLength() - kWavHeaderSize), read);

            VerifyDataRecording(source_interleaved_.get(), result_interleaved.get(),
                source_samples_);
        }
    }

    void TestDoneOnFileThread(const base::Closure& callback)
    {
        DCHECK_CURRENTLY_ON(BrowserThread::FILE);

        callback.Run();
    }

    void DoDebugRecording()
    {
        // Write tasks are posted to BrowserThread::FILE.
        for (int i = 0; i < writes_; ++i) {
            std::unique_ptr<media::AudioBus> bus = media::AudioBus::Create(
                params_.channels(), params_.frames_per_buffer());

            bus->FromInterleaved(
                source_interleaved_.get() + i * params_.channels() * params_.frames_per_buffer(),
                params_.frames_per_buffer(), kBytesPerSample);

            input_debug_writer_->Write(std::move(bus));
        }
    }

    void WaitForRecordingCompletion()
    {
        media::WaitableMessageLoopEvent event;

        // Post a task to BrowserThread::FILE indicating that all the writes are
        // done.
        BrowserThread::PostTask(
            BrowserThread::FILE, FROM_HERE,
            base::Bind(&AudioDebugFileWriterTest::TestDoneOnFileThread,
                base::Unretained(this), event.GetClosure()));

        // Wait for TestDoneOnFileThread() to call event's closure.
        event.RunAndWait();
    }

    void RecordAndVerifyOnce()
    {
        base::FilePath file_path;
        EXPECT_TRUE(base::CreateTemporaryFile(&file_path));

        input_debug_writer_->Start(file_path);

        DoDebugRecording();

        input_debug_writer_->Stop();

        WaitForRecordingCompletion();

        VerifyRecording(file_path);

        if (::testing::Test::HasFailure()) {
            LOG(ERROR) << "Test failed; keeping recording(s) at ["
                       << file_path.value().c_str() << "].";
        } else {
            EXPECT_TRUE(base::DeleteFile(file_path, false));
        }
    }

protected:
    TestBrowserThreadBundle thread_bundle_;

    // Writer under test.
    std::unique_ptr<AudioDebugFileWriter> input_debug_writer_;

    // AudioBus parameters.
    media::AudioParameters params_;

    // Number of times to write AudioBus to the file.
    int writes_;

    // Number of samples in the source data.
    int source_samples_;

    // Source data.
    std::unique_ptr<int16_t[]> source_interleaved_;

private:
    DISALLOW_COPY_AND_ASSIGN(AudioDebugFileWriterTest);
};

class AudioDebugFileWriterBehavioralTest : public AudioDebugFileWriterTest {
};

TEST_P(AudioDebugFileWriterTest, WaveRecordingTest)
{
    input_debug_writer_.reset(new AudioDebugFileWriter(params_));

    RecordAndVerifyOnce();
}

TEST_P(AudioDebugFileWriterBehavioralTest,
    DeletedBeforeRecordingFinishedOnFileThread)
{
    input_debug_writer_.reset(new AudioDebugFileWriter(params_));

    base::FilePath file_path;
    EXPECT_TRUE(base::CreateTemporaryFile(&file_path));

    base::WaitableEvent* wait_for_deletion = new base::WaitableEvent(base::WaitableEvent::ResetPolicy::MANUAL,
        base::WaitableEvent::InitialState::NOT_SIGNALED);

    BrowserThread::PostTask(
        BrowserThread::FILE, FROM_HERE,
        base::Bind(&base::WaitableEvent::Wait, base::Owned(wait_for_deletion)));

    input_debug_writer_->Start(file_path);

    DoDebugRecording();

    input_debug_writer_.reset();
    wait_for_deletion->Signal();

    WaitForRecordingCompletion();

    VerifyRecording(file_path);

    if (::testing::Test::HasFailure()) {
        LOG(ERROR) << "Test failed; keeping recording(s) at ["
                   << file_path.value().c_str() << "].";
    } else {
        EXPECT_TRUE(base::DeleteFile(file_path, false));
    }
}

TEST_P(AudioDebugFileWriterBehavioralTest, FileCreationError)
{
    input_debug_writer_.reset(new AudioDebugFileWriter(params_));
    base::FilePath file_path; // Empty file name.
    input_debug_writer_->Start(file_path);
    DoDebugRecording();
}

TEST_P(AudioDebugFileWriterBehavioralTest, StartStopStartStop)
{
    input_debug_writer_.reset(new AudioDebugFileWriter(params_));
    RecordAndVerifyOnce();
    RecordAndVerifyOnce();
}

TEST_P(AudioDebugFileWriterBehavioralTest, DestroyNotStarted)
{
    input_debug_writer_.reset(new AudioDebugFileWriter(params_));
    input_debug_writer_.reset();
}

TEST_P(AudioDebugFileWriterBehavioralTest, DestroyStarted)
{
    input_debug_writer_.reset(new AudioDebugFileWriter(params_));
    base::FilePath file_path;
    EXPECT_TRUE(base::CreateTemporaryFile(&file_path));
    input_debug_writer_->Start(file_path);
    input_debug_writer_.reset();
}

INSTANTIATE_TEST_CASE_P(
    AudioDebugFileWriterTest,
    AudioDebugFileWriterTest,
    // Using 10ms frames per buffer everywhere.
    testing::Values(
        // No writes.
        std::tr1::make_tuple(media::ChannelLayout::CHANNEL_LAYOUT_MONO,
            44100,
            44100 / 100,
            0),
        // 1 write of mono.
        std::tr1::make_tuple(media::ChannelLayout::CHANNEL_LAYOUT_MONO,
            44100,
            44100 / 100,
            1),
        // 1 second of mono.
        std::tr1::make_tuple(media::ChannelLayout::CHANNEL_LAYOUT_MONO,
            44100,
            44100 / 100,
            100),
        // 1 second of mono, higher rate.
        std::tr1::make_tuple(media::ChannelLayout::CHANNEL_LAYOUT_MONO,
            48000,
            48000 / 100,
            100),
        // 1 second of stereo.
        std::tr1::make_tuple(media::ChannelLayout::CHANNEL_LAYOUT_STEREO,
            44100,
            44100 / 100,
            100),
        // 15 seconds of stereo, higher rate.
        std::tr1::make_tuple(media::ChannelLayout::CHANNEL_LAYOUT_STEREO,
            48000,
            48000 / 100,
            1500)));

INSTANTIATE_TEST_CASE_P(
    AudioDebugFileWriterBehavioralTest,
    AudioDebugFileWriterBehavioralTest,
    // Using 10ms frames per buffer everywhere.
    testing::Values(
        // No writes.
        std::tr1::make_tuple(media::ChannelLayout::CHANNEL_LAYOUT_MONO,
            44100,
            44100 / 100,
            100)));

} // namespace content
